node.jsのいろいろなモジュール17 – asyncで非同期処理のフロー制御
node.jsでの関数呼び出し
非同期処理
node.jsはnon-blocking処理を行うため、関数によってはプログラマの意図しない順番で処理が実行されることがあります。 次の例をみてください。プログラムの開始をconsole.logで出力後、ファイルを読み込んで内容を出力、 最後にプログラム終了をconsoke.logで出力しています。
//app.js var fs = require('fs'); console.log("start"); //ファイル読み込み fs.readFile("hello.txt","utf-8",function(err,data) { console.log(data); }); console.log("finish");
上記プログラムを自分の環境で実行した結果は次のようになってます。 startとfinishが表示され、ファイル内容は最後に表示されています。環境によってはfinishより先にファイル内容の出力があるかもしれません。
% node app.js start finish hello ←ファイルの内容
例にあるreadFileは非同期APIなので、ファイルを読み込んだ後に処理をしたい場合は、 第2引数のコールバック関数内で処理を記述する必要があります。 ※同期版APIであるreadFileSyncを使えば記述どおりの順番で処理が実行されますが、今回は使用しない
順番に処理したい(間違った例)
もう1つ例を考えてみましょう。次の例では、順番にファイルA、ファイルB、ファイルCと読み込んでいく処理を実現したいと思っています。 次のようにファイルを読み込むプログラムを記述しました。
var fs = require('fs'); console.log("start"); fs.readFile("file-A","utf-8",function(err,data) { //ファイルAを読みこんで何かする処理 }); fs.readFile("file-B","utf-8",function(err,data) { //ファイルBを読みこんで何かする処理 }); fs.readFile("file-C","utf-8",function(err,data) { //ファイルCを読みこんで何かする処理 }); console.log("finish");
理想としては、上から順番にファイルA、ファイルB、ファイルCと読み込んでいきたいところですが、 先ほどの例を見てもわかるように、readFile関数が実行される順番は保証されていません。
順番に処理したい(コールバックをネスト)
readFile関数を使ってファイルABCを順場に読ませるには、次のようにコールバックをネストさせて記述します。
var fs = require('fs'); console.log("start"); fs.readFile("file-A","utf-8",function(err,data) { //ファイルAを読みこんで何かする処理 fs.readFile("file-B","utf-8",function(err,data) { //ファイルBを読みこんで何かする処理 fs.readFile("file-C","utf-8",function(err,data) { //ファイルCを読みこんで何かする処理 }); }); }); console.log("finish");
このようにすれば、ファイルA〜Cの読み込みは順番に行われます。 しかし、このような記述方法は処理が複雑になればなるほどネストが深くなり、非常に可読性が下がります。 このような非同期処理を順番に、綺麗に記述するためのモジュールが、今回紹介するasyncモジュールです。
asyncモジュール
asyncモジュールとは、複数の非同期処理のフロー制御が可能になります。 非同期APIを指定した順番で実行したり、処理結果を受け取って次の関数へ渡したりすることもできます。 このモジュールにはmapとかreduceとか便利な関数がいっぱいあるのですが、今回は基本的なseries関数とwaterfall関数を使ってみましょう。
環境構築方法
今回使用した動作環境は以下のとおりです。
- OS : MacOS X 10.7.4
- Node.js : v0.8.15
- npm : 1.1.66
適当なディレクトリを作成し、そこでnpmを使用して必要なモジュールをインストールします。
% mkdir async % cd async % npm install async % touch app.js
サンプルプログラム作成
seriesで順番に実行
まずは非同期API処理を順番に行ってみましょう。series関数を使えば、複数の関数を順番に実行できます。 先ほどの処理をasyncで書き直しました。どんなに非同期APIの呼び出しが多くなっても、ネストが一定以上深くなりません。
var fs = require('fs'); var async = require('async'); async.series([ function (callback) { fs.readFile("file-A","utf-8",function(err,data) { console.log("file-A"); callback(null, "first"); }); }, function (callback) { fs.readFile("file-B","utf-8",function(err,data) { console.log("file-B"); callback("e", "second") }); }, function (callback) { fs.readFile("file-C","utf-8",function(err,data) { console.log("file-C"); callback(null, "third") }); } ], function (err, results) { if (err) { throw err; } console.log('series all done. ' + results); });
asyncモジュールのseries関数の第1引数には順番に実行したい関数の配列を渡します。 第2引数の関数は、すべての関数が実行後、最後に実行されます。 各関数の最後にcallbackを呼び出しています。これが次の関数の呼び出しになります。 callbackの第1引数はエラー発生時、第2引数は結果をスタックしておき、最後の関数内で確認することができます。
waterfallで引数を受け取る
waterfall関数を使用すると、前の関数から引数を受け取ることができます。 次の例では引数を渡し、次の関数で渡された値にアクセスしていますね。
var async = require('async'); async.waterfall([ function(callback){ console.log("--first"); callback(null, 'one', 'two'); }, function(arg1, arg2, callback){ console.log("--second"); console.log("arg1:" + arg1); console.log("arg2:" + arg2); callback(null, 'three'); }, function(arg1, callback){ console.log("--third"); console.log("arg1:" + arg1); callback(null, 'done'); } ], function (err, result) { if(err) { throw err; } console.log('waterfall all done. ',result); });
前の関数の結果を受けて処理内容が変わる場合はwatarfall関数を使いましょう。
まとめ
今回は非同期APIでフロー制御をサポートするためのモジュール、asyncを紹介しました。 アプリケーションの初期化時など、処理の順番が重要なときは使うこともありそうですね。 このモジュールにはまだまだ便利機能がありますので、下記githubサイトをご確認ください。
参考サイトなど
- Github: https://github.com/caolan/async